﻿using ACMESharp.Authorizations;
using ACMESharp.Crypto.JOSE;
using ACMESharp.Protocol;
using ACMESharp.Protocol.Resources;
using Microsoft.Extensions.Options;
using PKISharp.SimplePKI;
using ProxyServer.AliDns;
using ProxyServer.Extensions;
using System.Net;
using System.Security.Cryptography.X509Certificates;

namespace ProxyServer.Acme;

public class BeginServiceCertificateState : SyncServiceAcmeState
{
    public BeginServiceCertificateState(ILogger<BeginServiceCertificateState> logger, IOptionsSnapshot<ServiceAcmeOptions> options, AcmeStateMachineContext context) : base(logger, options, context)
    {

    }

    public override IAcmeState MoveNext()
    {
        if (this.DomainNames.IsNullOrEmpty())
        {
            _logger.LogWarning("订单 {Order} 未指定域名", this.OrderId);
            return MoveTo<TerminalState>();
        }

        return MoveTo<CreateServiceCertificateOrderState>();
    }
}

public class CreateServiceCertificateOrderState : ServiceAcmeState
{
    private readonly AcmeClientFactory _acmeFactory;
    private readonly IAcmeStore _store;

    public CreateServiceCertificateOrderState(
        AcmeClientFactory factory,
        IAcmeStore store,
        ILogger<CreateServiceCertificateOrderState> logger,
        IOptionsSnapshot<ServiceAcmeOptions> service,
        AcmeStateMachineContext context)
        : base(logger, service, context)
    {
        _acmeFactory = factory;
        _store = store;
    }

    public override async Task<IAcmeState> MoveNextAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Start check order {Id}", this.OrderId);

        var order = await _store.LoadAsync<OrderDetails>(AcmeStoreConstants.ObjectKeys.AcmeOrder, cancellationToken, this.OrderId);

        var acme = await _acmeFactory.Create(cancellationToken: cancellationToken);

        if (order is null)
        {
            order = await acme.CreateOrderAsync(this.DomainNames, cancel: cancellationToken);
            _logger.LogInformation("Created new order {Id}", order.OrderUrl);
        }
        else
        {
            order = await acme.GetOrderDetailsAsync(order.OrderUrl, existing: order, cancellationToken);
            _logger.LogInformation("Loaded existing order {Id}", order.OrderUrl);
        }

        await _store.SaveAsync(order, AcmeStoreConstants.ObjectKeys.AcmeOrder, cancellationToken, OrderId);

        while (!cancellationToken.IsCancellationRequested)
        {
            switch (order.Payload.Status)
            {
                case "invalid":
                    _logger.LogWarning("订单状态{Status}，无法完成", order.Payload.Status);
                    return MoveTo<TerminalState>();

                case "ready":
                    return MoveTo<FinalizeServiceOrderState>();

                case "valid":
                    return MoveTo<ExportServiceCertificateOrderState>();

                case "pending":
                    return MoveTo<ChallengeServiceOrderState>();

                default:
                    await Task.Delay(5 * 1000);
                    _logger.LogInformation("Order {Id} is {Status}, Refreshing...", OrderId, order.Payload.Status);
                    order = await acme.GetOrderDetailsAsync(order.OrderUrl, existing: order, cancellationToken);
                    break;
            }
        }

        return MoveTo<TerminalState>();
    }
}

public class ChallengeServiceOrderState : ServiceAcmeState
{
    private readonly AcmeClientFactory _acmeFactory;
    private readonly IAcmeStore _store;
    private readonly AliDnsChallengeProvider _challengeProvider;

    public ChallengeServiceOrderState(
        AcmeClientFactory acmeFactory, IAcmeStore store,
        ILogger<ChallengeServiceOrderState> logger,
        IOptionsSnapshot<ServiceAcmeOptions> options,
        AliDnsChallengeProvider challengeProvider,
        AcmeStateMachineContext context) : base(logger, options, context)
    {
        _acmeFactory = acmeFactory;
        _store = store;
        _challengeProvider = challengeProvider;
    }

    public override async Task<IAcmeState> MoveNextAsync(CancellationToken cancellationToken)
    {

        var order = await _store.LoadAsync<OrderDetails>(AcmeStoreConstants.ObjectKeys.AcmeOrder, cancellationToken, OrderId);

        if (order is null || order.Payload.Status != AcmeStoreConstants.PendingStatus)
        {
            return MoveTo<TerminalState>();
        }

        var authzStatusCounts = new Dictionary<string, int>
        {
            [AcmeStoreConstants.ValidStatus] = 0,
            [AcmeStoreConstants.InvalidStatus] = 0,
            [AcmeStoreConstants.ValidStatus] = 0,
            ["unknown"] = 0,
        };
        void AddStatusCount(string status, int add)
        {
            if (!authzStatusCounts.ContainsKey(status))
                status = "unknown";
        }

        var accountKey = await _store.LoadAsync<AccountKey>(AcmeStoreConstants.ObjectKeys.AcmeAccountKey, cancellationToken);
        if (accountKey is null)
        {
            _logger.LogWarning("AccountKey is not exists");
            return MoveTo<TerminalState>();
        }

        var accountSigner = accountKey.GenerateTool();

        var acme = await _acmeFactory.Create(cancellationToken: cancellationToken);

        foreach (var authzUrl in order.Payload.Authorizations)
        {
            var authzId = AcmeHelper.ComputeHash(authzUrl);
            var authz = await _store.LoadAsync<ACMESharp.Protocol.Resources.Authorization>(AcmeStoreConstants.ObjectKeys.AcmeOrderAuthz, cancellationToken, OrderId, authzId);

            if (authz is null || this.RefreshOrder)
            {
                authz = await acme.GetAuthorizationDetailsAsync(authzUrl, cancellationToken);
                await _store.SaveAsync(authz, AcmeStoreConstants.ObjectKeys.AcmeOrderAuthz, cancellationToken, OrderId, authzId);
            }

            AddStatusCount(authz.Status, 1);

            foreach (var challenge in authz.Challenges)
            {
                if (challenge.Type.Equals("dns-01", StringComparison.OrdinalIgnoreCase) == false)
                {
                    _logger.LogInformation("Skipping {ChallengeType} {status} {token}", challenge.Type, challenge.Status, challenge.Token);
                    continue;
                }

                ACMESharp.Protocol.Resources.Authorization? authzUpdated = null;
                Challenge? challengeUpdated = await acme.GetChallengeDetailsAsync(challenge.Url, cancellationToken);

                var cd = AuthorizationDecoder.DecodeChallengeValidation(authz, challenge.Type, accountSigner);

                var processed = await ProcessDns01(accountSigner, authz, challengeUpdated, cd, cancellationToken);

                if (processed)
                {
                    // answer challenge
                    await Task.Delay(10 * 1000);

                    challengeUpdated = await acme.AnswerChallengeAsync(challenge.Url, cancellationToken);
                    authzUpdated = await acme.GetAuthorizationDetailsAsync(authzUrl, cancellationToken);

                    _logger.LogInformation("Answer Challenge: {ChallengeUrl} Status: {Status} Validated {Validated} Error {@Error}"
                        , challengeUpdated.Url, challengeUpdated.Status, challengeUpdated.Validated, challengeUpdated.Error);   
                }

                if (challengeUpdated != null)
                {
                    await _store.SaveAsync(challengeUpdated, AcmeStoreConstants.ObjectKeys.AcmeOrderAuthzChallenge, cancellationToken, OrderId, authzId, challenge.Type);
                }

                if (authzUpdated != null)
                {
                    if (authzUpdated.Status != authz.Status)
                    {
                        AddStatusCount(authz.Status, -1);
                        AddStatusCount(authzUpdated.Status, 1);
                    }

                    authz = authzUpdated;
                    await _store.SaveAsync(authz, AcmeStoreConstants.ObjectKeys.AcmeOrderAuthz, cancellationToken, OrderId, authzId);
                    authzUpdated = null;
                }
            }
        }

        int validStatusCount = 0;
        var waitUntil = DateTime.Now.AddSeconds(_options.Value.TimeoutSeconds);

        foreach (var authzUrl in order.Payload.Authorizations)
        {
            var authz = await acme.GetAuthorizationDetailsAsync(authzUrl, cancellationToken);
            while (authz.Status != AcmeStoreConstants.ValidStatus && DateTime.Now < waitUntil)
            {
                _logger.LogDebug("Current {Status} Waiting...{authUrl}", authz.Status, authzUrl);
                await Task.Delay(1 * 1000);
                authz = await acme.GetAuthorizationDetailsAsync(authzUrl, cancellationToken);
            }

            if (authz.Status == AcmeStoreConstants.ValidStatus)
            {
                _logger.LogInformation("Challenge succeeded {authUrl}", authzUrl);
                validStatusCount += 1;
            }
        }

        _logger.LogDebug("{@ValidStatusCount}", validStatusCount);

        // TODO: 状态未全部正确
        if (validStatusCount < order.Payload.Authorizations.Length)
        {
            _logger.LogWarning("无法完结订单{Id}：订单所有验证未都通过", OrderId);
            return MoveTo<TerminalState>();
        }

        return MoveTo<FinalizeServiceOrderState>();
    }

    private async Task<bool> ProcessDns01(IJwsTool accountSigner, ACMESharp.Protocol.Resources.Authorization authz, Challenge challenge, IChallengeValidationDetails cd, CancellationToken cancellationToken)
    {
        var answered = false;

        var testUtil = DateTime.Now.AddSeconds(_options.Value.TimeoutSeconds);

        var context = new DnsTxtRecordContext(authz.Identifier.Value, string.Empty);

        try
        {
            var dnsCd = (Dns01ChallengeValidationDetails)cd;
            var dnsCdValue = dnsCd.DnsRecordValue;

            _logger.LogInformation("Challenging {ChallengeType} Record: Name {Name} Type {Type} Value {Value}"
                , dnsCd.ChallengeType
                , dnsCd.DnsRecordName
                , dnsCd.DnsRecordType
                , dnsCdValue);

            var old = await _store.LoadAsync<DnsTxtRecordContext>(AcmeStoreConstants.ObjectKeys.DomainRecord, cancellationToken, OrderId);
            if (old is not null)
            {
                await _challengeProvider.RemoveTxtRecordAsync(old, cancellationToken);
            }

            if (challenge.Status == AcmeStoreConstants.ValidStatus)
            {
                return answered;
            }

            context = await _challengeProvider.AddTxtRecordAsync(dnsCd.DnsRecordName, dnsCdValue, cancellationToken);
            await _store.SaveAsync(context, AcmeStoreConstants.ObjectKeys.DomainRecord, cancellationToken, OrderId);

            while (!cancellationToken.IsCancellationRequested || DateTime.Now < testUtil)
            {
                string? err = null;

                try
                {
                    var lookup = await DnsUtil.LookupRecordAsync(dnsCd.DnsRecordType, dnsCd.DnsRecordName, cancellationToken);
                    var dnsValues = lookup?.Select(x => x.Trim('"'));

                    if (dnsValues.IsNullOrEmpty())
                    {
                        err = "Could not resolve *any* DNS entries for Challenge record name";
                    }
                    else if (dnsValues.Contains(dnsCdValue) == false)
                    {
                        var dnsValuesFlattened = string.Join(",", dnsValues);
                        err = $"DNS entry does not match expected value for Challenge record name ({dnsCdValue} not in {dnsValuesFlattened})";
                    }
                    else
                    {
                        err = null;
                        _logger.LogInformation("Found expected DNS entry for Challenge record name: {@names}", dnsValues);

                        var acme = await _acmeFactory.Create();
                        var ch = await acme.AnswerChallengeAsync(challenge.Url, cancellationToken);

                        _logger.LogInformation("Answered Challenge: {ChallengeUrl} Status: {Status} Validated {Validated} Error {@Error}", challenge.Url, ch.Status, ch.Validated, ch.Error);
                        answered = true;
                        break;
                    }
                }
                catch (Exception ex1)
                {
                    err = ex1.Message;
                    _logger.LogDebug(ex1, "Checking DNS01 Failed");
                }

                if(err is not null)
                {
                    _logger.LogWarning("Failed: {error}", err);
                }

                if (DateTime.Now < testUtil)
                {
                    await Task.Delay(5 * 1000);
                    _logger.LogInformation("Waiting...");
                }
            }

        }
        catch(Exception ex)
        {
            _logger.LogError(ex, null);
        }
        finally
        {
            await _challengeProvider.RemoveTxtRecordAsync(context, cancellationToken);
            await _store.RemoveAsync(AcmeStoreConstants.ObjectKeys.DomainRecord, cancellationToken, OrderId);
        }

        return answered;
    }
}


/// <summary>
/// Default txt record context
/// </summary>
public class DnsTxtRecordContext
{
    /// <summary>
    /// default constructor
    /// </summary>
    /// <param name="domainName">Domain name for the txt record</param>
    /// <param name="txt">TXT record Value</param>
    public DnsTxtRecordContext(string domainName, string txt)
    {
        DomainName = domainName;
        Txt = txt;
    }

    /// <summary>
    /// Domain name for the txt record
    /// </summary>
    public string DomainName { get; }
    /// <summary>
    /// TXT record Value
    /// </summary>
    public string Txt { get; }
}

public class FinalizeServiceOrderState : ServiceAcmeState
{
    private readonly AcmeClientFactory _acmeFactory;
    private readonly IAcmeStore _store;

    public FinalizeServiceOrderState(IAcmeStore store, AcmeClientFactory acmeFactory, ILogger<FinalizeServiceOrderState> logger, IOptionsSnapshot<ServiceAcmeOptions> options, AcmeStateMachineContext context) : base(logger, options, context)
    {
        _store = store;
        _acmeFactory = acmeFactory;
    }

    public override async Task<IAcmeState> MoveNextAsync(CancellationToken cancellationToken)
    {
        var order = await _store.LoadAsync<OrderDetails>(AcmeStoreConstants.ObjectKeys.AcmeOrder, cancellationToken, OrderId);
        if (order is null)
        {
            throw new InvalidOperationException($"订单{OrderId}不存在");
        }

        PkiKeyPair? keyPair = null;
        PkiCertificateSigningRequest? csr = null;

        var certKeys = await _store.LoadRawAsync<string>(AcmeStoreConstants.ObjectKeys.AcmeOrderCertKey, cancellationToken, OrderId);
        var certCsr = await _store.LoadRawAsync<byte[]>(AcmeStoreConstants.ObjectKeys.AcmeOrderCertCsr, cancellationToken, OrderId);

        if (keyPair is null || csr is null)
        {
            switch (KeyAlgor)
            {
                case AcmeStoreConstants.RsaKeyType:
                    keyPair = PkiKeyPair.GenerateRsaKeyPair(KeySize ?? AcmeStoreConstants.DefaultAlgorKeySizeMap[KeyAlgor]);
                    break;
                case AcmeStoreConstants.EcKeyType:
                    keyPair = PkiKeyPair.GenerateEcdsaKeyPair(KeySize ?? AcmeStoreConstants.DefaultAlgorKeySizeMap[KeyAlgor]);
                    break;
                default:
                    throw new Exception($"Unknown key algorithm type [{KeyAlgor}]");
            }


            csr = GenerateCsr(_options.Value.Service.Hosts, keyPair);
            certKeys = Save(keyPair);
            certCsr = csr.ExportSigningRequest(PkiEncodingFormat.Der);

            await _store.SaveRawAsync(certKeys, AcmeStoreConstants.ObjectKeys.AcmeOrderCertKey, cancellationToken, OrderId);
            await _store.SaveRawAsync(certCsr, AcmeStoreConstants.ObjectKeys.AcmeOrderCertCsr, cancellationToken, OrderId);
        }

        _logger.LogInformation("Finalizing Order...");
        var acme = await _acmeFactory.Create(cancellationToken: cancellationToken);
        order = await acme.FinalizeOrderAsync(order.Payload.Finalize, certCsr, cancellationToken);
        await _store.SaveAsync(order, AcmeStoreConstants.ObjectKeys.AcmeOrder, cancellationToken, OrderId);

        return MoveTo<CreateServiceCertificateOrderState>();
    }

    private string Save(PkiKeyPair keyPair)
    {
        using (var ms = new MemoryStream())
        {
            keyPair.Save(ms);
            return Convert.ToBase64String(ms.ToArray());
        }
    }

    private PkiCertificateSigningRequest GenerateCsr(IEnumerable<string> dnsNames,
        PkiKeyPair keyPair)
    {
        var firstDns = dnsNames.First();
        var csr = new PkiCertificateSigningRequest($"CN={firstDns}", keyPair,
            PkiHashAlgorithm.Sha256);

        csr.CertificateExtensions.Add(
            PkiCertificateExtension.CreateDnsSubjectAlternativeNames(dnsNames));

        return csr;
    }
}


public class ExportServiceCertificateOrderState : ServiceAcmeState
{
    private readonly IAcmeStore _store;
    private readonly AcmeClientFactory _acmeFactory;
    private readonly ICertificateRepository _certRepo;

    public ExportServiceCertificateOrderState(
        AcmeClientFactory acmeFactory,
        ICertificateRepository certRepo,
        IAcmeStore store,
        ILogger<ExportServiceCertificateOrderState> logger,
        IOptionsSnapshot    <ServiceAcmeOptions> options,
        AcmeStateMachineContext context) : base(logger, options, context)
    {
        _store = store;
        _acmeFactory = acmeFactory;
        _certRepo = certRepo;
    }

    public override async Task<IAcmeState> MoveNextAsync(CancellationToken cancellationToken)
    {
        if (await _store.ExistsAsync(AcmeStoreConstants.ObjectKeys.PfxFile, cancellationToken, OrderId))
        {
            return MoveTo<TerminalState>();
        }

        var order = await _store.LoadAsync<OrderDetails>(AcmeStoreConstants.ObjectKeys.AcmeOrder, cancellationToken, OrderId);
        if (order is null)
        {
            throw new InvalidOperationException($"未找到订单{OrderId}");
        }

        var acme = await _acmeFactory.Create(cancellationToken: cancellationToken);

        var certResp = await acme.GetAsync(order.Payload.Certificate);
        certResp.EnsureSuccessStatusCode();

        using (var ras = await certResp.Content.ReadAsStreamAsync(cancellationToken))
        {
            await _store.SaveRawAsync(ras, AcmeStoreConstants.ObjectKeys.AcmeOrderCert, cancellationToken, OrderId);
        }

        _logger.LogInformation("{OrderId} Exporting Certificate as PFX (PKCS12)...", OrderId);

        var pfxPassword = _options.Value.PfxPassword;

        var certKeys = await _store.LoadRawAsync<string>(AcmeStoreConstants.ObjectKeys.AcmeOrderCertKey, cancellationToken, OrderId);
        if (certKeys is null)
        {
            throw new InvalidOperationException($"{OrderId}: Certkeys 不存在");
        }

        var keyPair = Load(certKeys);
        var privateKey = keyPair.PrivateKey;

        if (string.IsNullOrWhiteSpace(pfxPassword))
        {
            pfxPassword = null; // with no password
        }

        var certBytes = await _store.LoadRawAsync<byte[]>(AcmeStoreConstants.ObjectKeys.AcmeOrderCert, cancellationToken, OrderId);

        if (certBytes is null)
        {
            throw new InvalidOperationException($"{OrderId}: 证书不存在");
        }

        using (var cert = X509CertificateLoader.LoadCertificate(certBytes))
        {
            var pkiCert = PkiCertificate.From(cert);
            var pfx = pkiCert.Export(PkiArchiveFormat.Pkcs12, privateKey: privateKey, password: pfxPassword?.ToCharArray());

            await _store.SaveRawAsync(pfx, AcmeStoreConstants.ObjectKeys.PfxFile, cancellationToken, OrderId);
        }

        return MoveTo<TerminalState>();
    }

    private PkiKeyPair Load(string b64)
    {
        using (var ms = new MemoryStream(Convert.FromBase64String(b64)))
        {
            return PkiKeyPair.Load(ms);
        }
    }
}
